package cz.ctu.guiproject.client.manager;

import static cz.ctu.guiproject.client.resources.constants.Constants.BUFFER_SIZE;
import static cz.ctu.guiproject.client.resources.constants.Constants.COMPONENTS_PATH;
import static cz.ctu.guiproject.client.resources.constants.Constants.DEX;
import static cz.ctu.guiproject.client.resources.constants.Constants.DOT;
import static cz.ctu.guiproject.client.resources.constants.Constants.FORWARD_SLASH;
import static cz.ctu.guiproject.client.resources.constants.Constants.JAR_EXT;
import static cz.ctu.guiproject.client.resources.constants.Constants.JAVA_EXT;
import static cz.ctu.guiproject.client.resources.constants.Logger.TAG;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.xml.sax.Attributes;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.View;
import cz.ctu.guiproject.client.dispatcher.Dispatcher;
import cz.ctu.guiproject.client.dispatcher.DispatcherImpl;
import cz.ctu.guiproject.client.resources.ActivityProvider;
import cz.ctu.guiproject.client.resources.ContextProvider;
import cz.ctu.guiproject.client.resources.InstalledComponent;
import cz.ctu.guiproject.client.resources.utils.FileHandler;
import cz.ctu.guiproject.client.xml.XmlDecodeFactory;
import cz.ctu.guiproject.client.xml.XmlEncodeFactory;
import dalvik.system.DexClassLoader;

public final class JARManagerImpl implements JARManager {

	private static final Activity activity;
	private static final Context context;
	private static JARManager instance;
	private static final Map<String, Class<?>> classMap;
	private static final Map<String, guiproject.client.view.View> viewInstanceMap;

	static {
		activity = ActivityProvider.getInstance().getActivity();
		context = ContextProvider.getInstance().getContext();
		classMap = new TreeMap<String, Class<?>>();
		viewInstanceMap = new TreeMap<String, guiproject.client.view.View>();
	}

	private JARManagerImpl() {
	}

	public static JARManager getInstance() {
		if (instance == null) {
			instance = new JARManagerImpl();
		}
		return instance;
	}

	@Override
	public void setActivity(String key, Attributes attributes) {
		final Class<?> activityClass = classMap.get(key);
		if (activityClass == null) {
			Log.i(TAG, "No activity class found!");
			return;
		}

		Activity activity = null;
		try {

			final guiproject.client.view.Activity activityJar = (guiproject.client.view.Activity) activityClass
					.newInstance();
			activityJar.setActivity(activity);
			activityJar.setContext(context);
			final Dispatcher dispatcher = DispatcherImpl.getInstance();
			activityJar.setViewEventObserver(dispatcher);
			activityJar.setup(attributes);
			Log.i(TAG, "Activity setup completed!");
			// this should be enough
			
//			activity = 
//			
//			view = activityJar.getView(attributes);
//			viewInstanceMap.put(activityJar.getViewId(), activityJar);

		} catch (InstantiationException e) {
			Log.e(TAG, e.getMessage());
		} catch (IllegalAccessException e) {
			Log.e(TAG, e.getMessage());
		}
	}

	// TODO mozny vznik problemu - pokazde volana nova instance
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * cz.ctu.guiproject.client.manager.JARManager#getView(java.lang.String,
	 * org.xml.sax.Attributes)
	 */
	@Override
	public View getView(String key, Attributes attributes) {
		final Class<?> viewClass = classMap.get(key);
		if (viewClass == null) {
			return null;
		}

		View view = null;
		try {
			final guiproject.client.view.View viewJar = (guiproject.client.view.View) viewClass.newInstance();
			viewJar.setActivity(activity);
			final Dispatcher dispatcher = DispatcherImpl.getInstance();
			viewJar.setViewEventObserver(dispatcher);
			viewJar.setPropagateUIObserver(dispatcher);
			view = viewJar.getView(attributes);
			viewInstanceMap.put(viewJar.getViewId(), viewJar);

		} catch (InstantiationException e) {
			Log.e(TAG, e.getMessage());
		} catch (IllegalAccessException e) {
			Log.e(TAG, e.getMessage());
		}
		return view;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * cz.ctu.guiproject.client.manager.JARManager#setViewAttribute(java.lang
	 * .String, java.lang.String, java.lang.String)
	 */
	@Override
	public void setViewAttribute(final String viewId, final String attrName, final String attrValue) {
		final guiproject.client.view.View currentView = viewInstanceMap.get(viewId);
		if (currentView == null) {
			return;
		}
		final Object lock = new Object();

		activity.runOnUiThread(new Runnable() {
			@Override
			public void run() {
				currentView.setAttribute(attrName, attrValue);
				synchronized (lock) {
					lock.notify();
				}
			}
		});
		synchronized (lock) {
			try {
				// TODO changed from 1000
				lock.wait(200);
			} catch (InterruptedException e) {
				Log.i(TAG, e.getMessage());
			}
		}
	}

	// zohlednena verze instalovaneho JARu (to aby se vzdy dotahla ta
	// nejaktualnejsi z repository)
	/*
	 * (non-Javadoc)
	 * 
	 * @see cz.ctu.guiproject.client.manager.JARManager#getInstalledComponents()
	 */
	@Override
	public String[] getInstalledComponents() {
		// scan for contents of application folder
		String componentsXml;
		try {
			componentsXml = FileHandler.loadTextFile(COMPONENTS_PATH);
		} catch (FileNotFoundException ex) {
			Log.e(TAG, ex.getMessage());
			componentsXml = null;
		}
		if (componentsXml == null || componentsXml.equals("")) {
			return new String[] {};
		}
		final List<InstalledComponent> list = XmlDecodeFactory.decodeInstalledComponents(componentsXml);
		final String[] installedComponents = new String[list.size() * 2];
		int index = 0;
		for (InstalledComponent comp : list) {
			installedComponents[index++] = comp.getName();
			installedComponents[index++] = String.valueOf(comp.getVersion());
		}
		return installedComponents;
	}

	private String[] scanFolder(String baseFolder) {
		final File f = new File(baseFolder);
		final List<String> folderNames = new ArrayList<String>(Arrays.asList(f.list()));
		final Iterator<String> it = folderNames.iterator();
		while (it.hasNext()) {
			final String folderName = it.next();
			if (!folderName.endsWith(JAR_EXT)) {
				it.remove();
			}
		}
		return folderNames.toArray(new String[0]);
	}

	/*
	 * @see
	 * cz.ctu.guiproject.client.manager.JARManager#downloadComponents(java.lang
	 * .String[])
	 */
	@Override
	public void downloadComponents(String[] jarUrls) {
		String legacyComponentsXml;
		try {
			legacyComponentsXml = FileHandler.loadTextFile(COMPONENTS_PATH);
		} catch (FileNotFoundException ex) {
			Log.e(TAG, ex.getMessage());
			legacyComponentsXml = null;
		}
		final List<InstalledComponent> components;
		if (legacyComponentsXml == null || legacyComponentsXml.equals("")) {
			components = new LinkedList<InstalledComponent>();
		} else {
			components = XmlDecodeFactory.decodeInstalledComponents(legacyComponentsXml);
		}

		if (!(jarUrls == null || jarUrls.length == 0)) {
			InstalledComponent component;
			for (int i = 0; i < jarUrls.length; i += 3) {
				component = new InstalledComponent();
				final String filename = jarUrls[i] + JAR_EXT;
				final String url = jarUrls[i + 1];
				final double version = Double.parseDouble(jarUrls[i + 2]);
				Log.i(TAG, "Downloading component " + filename + ", version " + version);
				if (download(url, filename)) {
					Log.i(TAG, "Component " + filename + " downloaded successfully");

					// if successful, update config file
					component.setName(filename);
					component.setVersion(version);

					components.remove(component);
					components.add(component);

				} else {
					Log.e(TAG, "Component " + filename + " failed to download");
				}
			}

			// write updated config file into storage
			final String componentsXml = XmlEncodeFactory.encodeInstalledComponents(components);
			FileHandler.saveTextFile(COMPONENTS_PATH, componentsXml);
		}

		final String pathInternal = context.getFilesDir().toURI().getPath();
		// all required components are now downloaded, time to load them and use
		// them
		for (String jarFileName : scanFolder(pathInternal)) {
			Log.i(TAG, "Loading " + jarFileName);
			if (loadJar(pathInternal + jarFileName)) {
				Log.i(TAG, jarFileName + " loaded successfully");
			} else {
				Log.e(TAG, jarFileName + " failed to load");
			}
		}
	}

	// TODO situation, where jar contains more than one class file??
	private boolean loadJar(String jarFileName) {
		try {
			final JarFile jarFile = new JarFile(jarFileName);
			final Enumeration<?> e = jarFile.entries();

			final String dex_dir = context.getDir(DEX, 0).getAbsolutePath();
			final ClassLoader parent = getClass().getClassLoader();
			final DexClassLoader loader = new DexClassLoader(jarFileName, dex_dir, null, parent);

			while (e.hasMoreElements()) {
				final JarEntry je = (JarEntry) e.nextElement();
				if (je.isDirectory() || !je.getName().endsWith(JAVA_EXT)) {
					continue;
				}
				// trim file extension (.java)
				String className = je.getName().substring(0, je.getName().length() - JAVA_EXT.length());
				// replace forward slashes with dots (filesystem -> package)
				className = className.replace(FORWARD_SLASH, DOT);
				final Class<?> c = loader.loadClass(className);
				final String classNameTrim = className.substring(className.lastIndexOf(DOT) + 1);
				classMap.put(classNameTrim, c);
			}

			jarFile.close();
			return true;

		} catch (IOException e) {
			Log.e(TAG, e.getMessage());
		} catch (ClassNotFoundException e) {
			Log.e(TAG, e.getMessage());
		} catch (Exception e) {
			Log.e(TAG, e.getMessage());
		} catch (Throwable e) {
			Log.e(TAG, e.getMessage());
		}
		return false;
	}

	/**
	 * Download file from specified direct-download url and saves it to given
	 * file on device's filesystem
	 * 
	 * @param urlName
	 * @param filename
	 * @return
	 */
	private boolean download(String urlName, String filename) {
		try {
			final URL url = new URL(urlName);
			final BufferedInputStream in = new BufferedInputStream(url.openStream());
			final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
			final BufferedOutputStream out = new BufferedOutputStream(fos);

			final byte data[] = new byte[BUFFER_SIZE];
			int count;
			while ((count = in.read(data, 0, BUFFER_SIZE)) != -1) {
				out.write(data, 0, count);
			}
			out.flush();
			out.close();
			return true;
		} catch (MalformedURLException e) {
			Log.e(TAG, e.getMessage());
		} catch (IOException e) {
			Log.e(TAG, e.getMessage());
		} catch (Exception e) {
			Log.e(TAG, e.getMessage());
		} catch (Throwable t) {
			Log.e(TAG, t.getMessage());
		}
		return false;
	}

}
